package com.google.code.p.restrpc.core;

import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import org.aopalliance.intercept.MethodInvocation;
import org.springframework.aop.support.AopUtils;
import org.springframework.util.ClassUtils;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONException;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.parser.ParserConfig;

/**
 * @author dz
 */
class DefaultRpcServiceProxy implements RpcServiceProxy {
    
    private boolean registerTraceInterceptor = true;
    
    private RpcServiceMetadata metadata;
    
    private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader();
    
    private static final ConcurrentMap<String, Object> proxies = new ConcurrentHashMap<String, Object>();
    
    public DefaultRpcServiceProxy(RpcServiceMetadata metadata) {
        this.metadata = metadata;
    }
    
    public void setBeanClassLoader(ClassLoader classLoader) {
        this.beanClassLoader = classLoader;
    }
    
    private String getKey(String serviceName, String methodName, int paramSize) {
        return new StringBuilder(serviceName).append("#")
                                             .append(methodName)
                                             .append("#")
                                             .append(paramSize)
                                             .toString();
    }
    
    private Object getProxy(String targetKey) {
        Object objRef = proxies.get(targetKey);
        if (objRef == null) {
            proxies.putIfAbsent(targetKey, metadata.getService());
            objRef = proxies.get(targetKey);
        }
        return objRef;
    }
    
    /**
     * Return the ClassLoader that this accessor operates in, to be used for
     * deserializing and for generating proxies.
     */
    protected ClassLoader getBeanClassLoader() {
        return this.beanClassLoader;
    }
    
    public boolean isRegisterTraceInterceptor() {
        return registerTraceInterceptor;
    }
    
    public void setRegisterTraceInterceptor(boolean registerTraceInterceptor) {
        this.registerTraceInterceptor = registerTraceInterceptor;
    }
    
    public Object invoke(String methodName) throws Throwable {
        //
        Method method = determineMethod(metadata, methodName, 0);
        if (method == null) {
            throw new RpcServiceNotFoundException(methodName);
        }
        if (method.isAccessible()) {
            throw new RpcServiceNotAllowedException(methodName);
        }
        //
        String proxyKey = getKey(metadata.getServiceName(), methodName, 0);
        //
        // Object proxy = getProxy(proxyKey);
        //
        MethodInvocation methodInvocation = new DefaultMethodInvocation(metadata,
                                                                        method,
                                                                        new Object[0]);
        //
        return methodInvocation.proceed();
    }
    
    public Object invoke(String methodName, JSONArray jsonArray) throws Throwable {
        //
        Method method = determineMethod(metadata, methodName, jsonArray.size());
        if (method == null) {
            throw new RpcServiceException("The Method[name=" + methodName
                                          + "] is not available for RPC!");
        }
        //
        Object[] arguments = null;
        try {
            arguments = parseArguments(method, jsonArray);
        }
        catch (JSONException e) {
            throw new RpcServiceTransferFormatException("Convert JSON to Object failed!",
                                                        e);
        }
        //
        String proxyKey = getKey(metadata.getServiceName(),
                                 methodName,
                                 arguments.length);
        //
        // Object proxy = getProxy(proxyKey);
        //
        MethodInvocation methodInvocation = new DefaultMethodInvocation(metadata,
                                                                        method,
                                                                        arguments);
        //
        return methodInvocation.proceed();
    }
    
    public Object invoke(String methodName, JSONObject jsonObject) throws Throwable {
        //
        Method method = determineMethod(metadata, methodName, 1);
        if (method == null) {
            throw new RpcServiceNotFoundException("The Method[name=" + methodName
                                                  + "] is not available for RPC!");
        }
        //
        Object[] arguments = null;
        try {
            arguments = parseArguments(method, jsonObject);
        }
        catch (JSONException e) {
            throw new RpcServiceTransferFormatException("Convert JSON to Object failed!",
                                                        e);
        }
        //
        String proxyKey = getKey(metadata.getServiceName(),
                                 methodName,
                                 arguments.length);
        //
        // Object proxy = getProxy(proxyKey);
        //
        MethodInvocation methodInvocation = new DefaultMethodInvocation(metadata,
                                                                        method,
                                                                        arguments);
        //
        return methodInvocation.proceed();
    }
    
    private Object[] parseArguments(Method method, JSONObject jsonObject) {
        if (method.getParameterTypes().length != 1) {
            throw new IllegalArgumentException("Only one parameter support for " + method);
        }
        
        return new Object[] { JSON.toJavaObject(jsonObject,
                                                method.getParameterTypes()[0]) };
    }
    
    private Object[] parseArguments(Method method, JSONArray jsonArray) {
        if (method.getParameterTypes().length != jsonArray.size()) {
            throw new IllegalArgumentException("The size is not matched between method and jsonArray");
        }
        Object[] temp = jsonArray.toArray();
        
        Object[] result = new Object[method.getParameterTypes().length];
        Class[] paramTypes = method.getParameterTypes();
        for (int i = 0; i < paramTypes.length; i++) {
            if (ParserConfig.getGlobalInstance().isPrimitive(paramTypes[i])) {
                result[i] = jsonArray.getObject(i, paramTypes[i]);
                continue;
            }
            
            if (Map.class.isAssignableFrom(paramTypes[i]) || Collection.class.isAssignableFrom(paramTypes[i])) {
                result[i] = jsonArray.getObject(i, paramTypes[i]);
                continue;
            }
            
            if (paramTypes[i].isArray()) {
                Class componentType = paramTypes[i].getComponentType();
                // if
                // (ParserConfig.getGlobalInstance().isPrimitive(componentType))
                // {
                JSONArray subJsonArray = jsonArray.getJSONArray(i);
                // Object[] objs = jsonArray.getJSONArray(i).toArray();
                Object array = Array.newInstance(componentType,
                                                 subJsonArray.size());
                for (int j = 0; j < subJsonArray.size(); j++) {
                    Array.set(array,
                              j,
                              subJsonArray.getObject(j, componentType));
                }
                result[i] = array;
                // }
                // else {
                // Object[] objs = jsonArray.getJSONArray(i).toArray();
                // Object array = Array.newInstance(componentType, objs.length);
                // for (int j = 0; j < objs.length; j++) {
                // // ((JSONObject)objs[j]).
                // // Object item = JSON.toJavaObject((JSON) objs[j],
                // // componentType);
                // // Array.set(array, j, item);
                // }
                // result[i] = array;
                // }
                continue;
            }
            
            result[i] = JSON.toJavaObject(jsonArray.getJSONObject(i),
                                          paramTypes[i]);
        }
        return result;
    }
    
    private Method determineMethod(RpcServiceMetadata metadata,
                                   String methodName,
                                   int argSize) {
        boolean methodFound = false;
        Class[] classes = metadata.getServiceInterfaces();
        // TODO improve the performance,for instance:cache the methods
        Method matchedMethod = null;
        for (Class clazz : classes) {
            for (Method method : clazz.getMethods()) {
                if (method.getName().equals(methodName)) {
                    methodFound = true;
                    if (method.getParameterTypes().length == argSize) {
                        return method;
//                        matchedMethod = method;
//                        break;
                    }
                }
            }
            
//            if (methodFound && (matchedMethod != null)) {
//                break;
//            }
        }
        
        if (methodFound) {
            throw new RpcServiceParamNotMatchException("The params for method " + methodName
                                                       + " not matched!");
        }
//        if (methodFound && (matchedMethod == null)) {
//            throw new RpcServiceParamNotMatchException("The params for method " + methodName
//                                                       + " not matched!");
//        }
//        
//        Class serviceClass = metadata.getServiceClass();
//        for (Method method : serviceClass.getMethods()) {
//            if (method.getName().equals(methodName)) {
//                if (method.getParameterTypes().length == argSize) {
//                    return method;
//                }
//            }
//        }
//        
        return null;
    }
    
    class DefaultMethodInvocation implements MethodInvocation {
        
        protected final RpcServiceMetadata metadata;
        
        protected final Object target;
        
        protected final Method method;
        
        protected Object[] arguments;
        
        public DefaultMethodInvocation(RpcServiceMetadata metadata,
                                       Method method,
                                       Object[] arguments) {
            this.metadata = metadata;
            this.target = metadata.getService();
            this.method = method;
            this.arguments = arguments;
        }
        
        public Object getTarget() {
            return target;
        }
        
        public final Object getThis() {
            return this.target;
        }
        
        public final AccessibleObject getStaticPart() {
            return this.method;
        }
        
        /**
         * Return the method invoked on the proxied interface. May or may not
         * correspond with a method invoked on an underlying implementation of
         * that interface.
         */
        public final Method getMethod() {
            return this.method;
        }
        
        public final Object[] getArguments() {
            return (this.arguments != null ? this.arguments : new Object[0]);
        }
        
        public void setArguments(Object[] arguments) {
            this.arguments = arguments;
        }
        
        public Object proceed() throws Throwable {
            try {
                RpcServiceRepository.current(metadata);
                return AopUtils.invokeJoinpointUsingReflection(this.target,
                                                               this.method,
                                                               this.arguments);
            }
            finally {
                RpcServiceRepository.clear();
            }
        }
        
    }
    
}
